use crate::util::Util;
use proto::build_node::build_hot_update_service_server::BuildHotUpdateService;
use proto::build_node::{
    GetParameterRequest, GetParameterResponse, NotifyTaskResponse, get_parameter_response,
    notify_task_response,
};
use proto::common::BuildLog;
use share::core::resource_version::ResourceVersion;
use snafu::{ResultExt, Snafu};
use tokio::fs::{File, OpenOptions};
use tokio::io::AsyncWriteExt;
use core::task;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard, TryLockError, TryLockResult};
use tokio::process::Command;
use tonic::{Request, Response, Status, Streaming, async_trait};
use tracing::dispatcher::get_default;
use tracing::error;
use uuid::Uuid;
use tokio_stream::StreamExt;

#[derive(Debug, Snafu)]
pub enum Error {
    #[snafu(display("Another task is processing"))]
    Busy,
    #[snafu(display("Failed to open log, path: {path}"))]
    OpenLog {
        path: String
    },
    #[snafu(display("Failed to read log entry"))]
    RunUnity {
        source: std::io::Error,
    }
}

pub type Result<T> = std::result::Result<T, Error>;
pub type RpcResult<T> = std::result::Result<T, Status>;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TaskState {
    Waiting,
    Working,
    Disconnect,
    Completed,
}

struct TaskInfo {
    id: Uuid,
    resource_version: ResourceVersion,
    build_raw: bool,
    build_default: bool,

    state: TaskState,
}

impl TaskInfo {
    pub fn log_path(&self) -> PathBuf {
        Util::app_dirs()
            .config_dir()
            .join("task_log")
            .join(format!("{}.log", self.id.to_string()))
    }

    pub fn check_authority(&self, id: &Uuid, addr: &SocketAddr) -> bool {
        &self.id == id
    }
    
    pub async fn open_log_file(&self) -> Result<File> {
        let path = self.log_path();
        OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)
            .await
            .map_err(|e| {
                error!(%e);
                Error::OpenLog {
                    path: path.display().to_string()
                }
            })
    }
}

async fn write_log(f: &mut File, log: BuildLog) {
    match serde_json::to_string(&log) {
        Ok(s) => {
            match f.write_all(s.as_bytes()).await {
                Ok(_) => {},
                Err(e) => {
                    error!(%e, "Failed to write log")
                },
            }
        },
        Err(e) => {
            error!(%e, "Failed to serialize log")
        },
    }
}

pub struct BuildService {
    unity: String,
    port: u16,
    current_task: Arc<Mutex<Option<TaskInfo>>>,
}

impl BuildService {
    pub fn new(unity: String, port: u16) -> Self {
        Self {
            unity,
            port,
            current_task: Arc::new(Mutex::new(None)),
        }
    }

    pub fn current_task(&self) -> std::sync::MutexGuard<'_, Option<TaskInfo>> {
        self.current_task.lock().unwrap()
    }

    pub fn is_available(&self) -> bool {
        let Ok(g) = self.current_task.try_lock() else {
            return false;
        };

        (*g).is_none()
    }

    pub fn build_hot_update(
        &self,
        resource_version: ResourceVersion,
        build_raw: bool,
        build_default: bool,
    ) -> Result<Uuid> {
        let g = self.current_task.try_lock()
            .ok()
            .filter(|g| g.is_none())
            .ok_or(Error::Busy)?;


        let id = Uuid::new_v4();
        let task = TaskInfo {
            id,
            resource_version,
            build_raw,
            build_default,

            state: TaskState::Waiting,
        };

        Command::new(&self.unity)
            .arg("-batchmode")
            .arg("-nographics")
            .arg("-quit")
            .arg("-projectPath /path/to/your/project")
            .arg("-executeMethod Sorani.BuildTool.BuildUtils.BuildFromConsole")
            .arg("-BuildHost=localhost")
            .arg(format!("-BuildPort={}", id))
            .spawn()
            .context(RunUnitySnafu)?;

        (*g).replace(task);
        Ok(id)
    }
}

impl BuildService {
    fn get_parameter(
        &self,
        request: Request<GetParameterRequest>,
    ) -> std::result::Result<get_parameter_response::Result, get_parameter_response::Error> {
        let addr = match request.remote_addr() {
            Some(a) => a,
            None => {
                error!("Failed to get remote addr from request");
                return Err(get_parameter_response::Error::Unknown);
            },
        };

        let request = request.into_inner();
        let id = Uuid::parse_str(&request.id)
            .map_err(|_| get_parameter_response::Error::InvalidId)?;

        let guard = self.current_task();
        let task = (*guard)
            .as_ref()
            .ok_or(get_parameter_response::Error::TaskNotFound)?;

        if !task.check_authority(&id, &addr) {
            Err(get_parameter_response::Error::NotAuthorized)
        } else {
            Ok(get_parameter_response::Result {
                id: task.id.to_string(),
                resource_version: task.resource_version.to_string(),
                build_raw: task.build_raw,
                build_default: task.build_raw,
            })
        }
    }

    async fn notify_task(
        &self,
        request: Request<Streaming<BuildLog>>
    ) -> std::result::Result<(), notify_task_response::Error> {
        let addr = request.remote_addr().ok_or_else(|| {
            error!("Failed to get remote addr from request");
            notify_task_response::Error::Unknown
        })?;

        let task = self.current_task.try_lock()
            .map_err(|_| notify_task_response::Error::Denied)
            .and_then(|g| g.as_ref().ok_or(notify_task_response::Error::TaskNotFound))?;

        let mut f = task.open_log_file().await
            .map_err(|e| {
                error!(%task.id, %e, "Failed to open log file");
                notify_task_response::Error::Unknown
            })?;

        let mut stream = request.into_inner();
        while let Some(log) = stream.next().await {
            let log = log.map_err(|e| {
                error!(%task.id, %e, "Failed to read log");
                notify_task_response::Error::Unknown
            })?;
            write_log(&mut f, log).await;
        }

        Ok(())
    }
}

#[async_trait]
impl BuildHotUpdateService for BuildService {
    async fn get_parameter(
        &self,
        request: Request<GetParameterRequest>,
    ) -> RpcResult<Response<GetParameterResponse>> {
        let inner = match self.get_parameter(request) {
            Ok(r) => get_parameter_response::Inner::R(r),
            Err(e) => get_parameter_response::Inner::E(e.into()),
        };

        Ok(Response::new(GetParameterResponse { inner: Some(inner) }))
    }

    async fn notify_task(
        &self,
        request: Request<Streaming<BuildLog>>,
    ) -> RpcResult<Response<NotifyTaskResponse>> {
        let inner = match self.notify_task(request).await {
            Ok(r) => { None }
            Err(e) => { Some(notify_task_response::Inner::E(e.into())) }
        };

        Ok(Response::new(NotifyTaskResponse { inner }))
    }
}


#[cfg!(test)]
mod test {
    use share::core::resource_version::ResourceVersion;
    use crate::build_service::BuildService;

    #[test]
    fn run_unity(){
        let build_service = BuildService::new("unity".to_string(), 6789);

        let version = ResourceVersion::new("abc").unwrap();
        build_service.build_hot_update(version, true, true).unwrap();


    }
}